package com.inibb.app.dao;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Criterion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.inibb.app.dao.api.IDao;
import com.inibb.app.dao.api.IEntity;
import com.inibb.app.dao.api.IInheritable;
import com.inibb.app.dao.sqlt.SqlTemplateEngine;
import com.inibb.app.exception.SqlTemplateInitException;
import com.inibb.app.exception.SqlTemplateParseException;

public class HibernateGenericDao<E, PK extends Serializable> implements IDao<E, PK> {

	private static final String PARAM_SEPARATOR = "_";
	
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	
	private SessionFactory sessionFactory;

	private Class<?> clazz;
	
	private Session getSession(){
		return sessionFactory.getCurrentSession();
	}

	/**
	 * Constructor with given {@link IEntity} implementation. Use for creting DAO without extending
	 * this class.
	 * 
	 * @param clazz class with will be accessed by DAO methods
	 */
	@SuppressWarnings(value = "unchecked")
	public HibernateGenericDao(SessionFactory sessionFactory,Class<?> clazz) {
		this.sessionFactory = sessionFactory;
		this.clazz = clazz;
//		super.setSessionFactory(sessionFactory);
	}

	@SuppressWarnings(value = "unchecked")
	@Override
	public E get(PK id) {
		return (E) getSession().get(clazz, id);
	}

	@SuppressWarnings(value = "unchecked")
	public List<E> getAll() {
		return findByCriteria();
	}	
	
	public List<E> findByCriteria (Criterion... criterion) {
		return createCriteria( criterion ).list( );
	}
	public Criteria createCriteria (Criterion... criterions) {
		Criteria criteria = getSession().createCriteria( clazz );
		for (Criterion c : criterions) {
			criteria.add( c );
		}
		return criteria;
	}

	@Override
	public E merge(final E object) {
		return (E) getSession().merge(object);
	}

	public E persist(final E object) {
		getSession().persist(object);
		return object;
}

	@Override
	public void delete(final PK id) throws UnsupportedOperationException {
		delete(get(id));
	}


	@Override
	public void delete(final E object) throws UnsupportedOperationException {
		getSession().delete(object);
	}

	@Override
	public void delete(final E... objects) throws UnsupportedOperationException {
		deleteAll(Arrays.asList(objects), true);
	}

	@Override
	public void deleteAll() throws UnsupportedOperationException {
//		deleteAll(getAll(), false);
	}

	private void deleteAll(final Collection<E> objects, boolean checkIdDefault)
	throws UnsupportedOperationException
	{
		for (E object : objects) {
			delete(object);
		}
	}

	@Override
	public void refresh(final E entity) {
		getSession().refresh(entity);
	}

	@Override
	public void flushAndClear() {
		getSession().flush();
		getSession().clear();
	}
	
	  /**
     * check whether the search criteria indicates to perform a fuzzy query. 
     * @param searchCriteria
     * @return
     */
    public boolean isFuzzyQuery(String searchCriteria){
    	
    	if(StringUtils.isEmpty(searchCriteria)) return false;
    	if(searchCriteria.indexOf("%")!=-1) return true;
    	   	    	
    	return false;
    }

	@Override
	public String getJpqlTemplate(String namespace, String sqltId, Object criteria) {
		try {
			return SqlTemplateEngine.getInstance().getJpql(namespace, sqltId, criteria);
		} catch (SqlTemplateParseException e) {
			throw new RuntimeException(e);
		} catch (SqlTemplateInitException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public String getNativeSqlTemplate(String namespace, String sqltId, Object criteria) {
		try {
			return SqlTemplateEngine.getInstance().getNativeSql(namespace, sqltId, criteria);
		} catch (SqlTemplateParseException e) {
			throw new RuntimeException(e);
		} catch (SqlTemplateInitException e) {
			throw new RuntimeException(e);
		}
	}
	
	@Override
	public Object queryNativeSqlForObject(String namespace, String sqltId, Object criteria){
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		Query query = getSession().createSQLQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.uniqueResult();
	}
	
	@Override
	public List queryNativeSqlForList(String namespace, String sqltId, Object criteria){
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		Query query = getSession().createSQLQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.list();
	}
	
	
	public List queryNativeSqlForList(String namespace, String sqltId, Object[] criteria){
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		Query query = getSession().createSQLQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.list();
	}
	
	@Override
	public List queryNativeSqlForList(String namespace, String sqltId,
			Object criteria, int startRow, int pageSize) {
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		Query query = getSession().createSQLQuery(qlString);
		setParameters(query, qlString, criteria);
		query.setFirstResult(startRow);
		query.setMaxResults(pageSize);
		return query.list();
	}
	
	@Override
	public int executeNativeSqlUpdate(String namespace, String sqltId,
			Object criteria) {
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		Query query = getSession().createSQLQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.executeUpdate();
	}
	
	@Override
	public int executeJpqlUpdate(String namespace, String sqltId,
			Object criteria) {
		String qlString = this.getJpqlTemplate(namespace, sqltId, criteria);
		Query query =  getSession().createQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.executeUpdate();
	}
	
	
	@Override
	public Object queryJpqlForObject(String namespace, String sqltId, Object criteria){
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getJpqlTemplate(namespace, sqltId, criteria);
		Query query = getSession().createQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.uniqueResult();
	}
	
	public Integer queryJpqlRowCount(String namespace, String sqltId, Object criteria){
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getJpqlTemplate(namespace, sqltId, criteria);
		String countQlString = "select count(*) from (" + qlString + ") temp";
		Query query = getSession().createQuery(countQlString);
		setParameters(query, countQlString, criteria);
		BigInteger bi = (BigInteger)query.uniqueResult();
		if(bi!=null)
			return bi.intValue();
		else
			return 0;
	}
	
	public Integer queryNativeSqlRowCount(String namespace, String sqltId, Object criteria){
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		//String countQlString = "SELECT count(*) FROM (" + qlString + ") TEMP";
		Query query = getSession().createSQLQuery(/*countQlString*/qlString);
		setParameters(query, /*countQlString*/qlString, criteria);
		BigInteger bi = (BigInteger)query.uniqueResult();
		if(bi!=null)
			return bi.intValue();
		else
			return 0;
	}
	
	@Override
	public List queryJpqlForList(String namespace, String sqltId, Object criteria){
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getJpqlTemplate(namespace, sqltId, criteria);
		Query query = getSession().createQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.list();
	}
    
	@Override
	public List queryJpqlForList(String namespace, String sqltId, Object criteria, int startRow, int pageSize){
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getJpqlTemplate(namespace, sqltId, criteria);
		Query query = getSession().createQuery(qlString);
		setParameters(query, qlString, criteria);
		query.setFirstResult(startRow);
		query.setMaxResults(pageSize);
		return query.list();
	}
	
	private void setParameters(Query query, String qlString, Object criteria){
		List<String> params =  this.resolveNamedParameters(qlString);
		try {
			for(String param: params){
				Object value = resolveParameterValue(param, criteria);
				query.setParameter(param, value);
				logger.trace("setting query parameter: {} = {}", param, value);
			}
			
		} catch (SqlTemplateParseException e) {
			throw new RuntimeException(e);
		}
		
	}
	private void setParameters(Query query, String qlString, Object[] criteria){
		List<String> params =  this.resolveNamedParameters(qlString);		
		int i=0;
		for(String param: params){				
			query.setParameter(param, criteria[i]);
			logger.trace("setting query parameter: {} = {}", param,  criteria[i]);
			i++;
		}	
	}
	
	private List<String> resolveNamedParameters(String qlString){
		List<String> params = new ArrayList<String>(0);
		String[] qlStrs = StringUtils.splitByWholeSeparator(qlString, null);
		for(String ql: qlStrs){
			if(ql!=null && ql.startsWith(":")){
				params.add(ql.replaceFirst(":", ""));
			}
		}
		return params;
	}
	
	private Object resolveParameterValue(String property, Object object) throws SqlTemplateParseException{
		try {
			if(property.indexOf(PARAM_SEPARATOR)!=-1){
				String resolvedProp = property.replaceAll(PARAM_SEPARATOR, "."); 
				return PropertyUtils.getNestedProperty(object, resolvedProp);
			}else{
				return PropertyUtils.getProperty(object, property);
			}
		} catch (IllegalAccessException e) {
			throw new SqlTemplateParseException(e);
		} catch (InvocationTargetException e) {
			throw new SqlTemplateParseException(e);
		} catch (NoSuchMethodException e) {
			throw new SqlTemplateParseException(e);
		}
	}
	
	

	
	public Object handleFuzzyQueryCriteria(Object example) {
		List<Integer> checkList = new ArrayList<Integer>();
		return traverseCriteriaObject(example, checkList);
	}
	
	private Object traverseCriteriaObject(Object example, List<Integer> checkList){

		
		if(example==null){
			return null;
		}
		
		//to avoid duplicated object traveling.
		if(checkList.contains(example.hashCode())){
			logger.trace("Object {} has been handled.", example);
			return example;
		}else{
			checkList.add(example.hashCode());
		}
		
		
		if(example instanceof Map){
			logger.trace("BeanUtils can not clone Map object.");
			return example;
		}
		
		
		if(example instanceof Collection){
			logger.trace("BeanUtils can not clone Collection object.");
			return example;
		}
		
		logger.trace("start to handle fuzzy query field....");
		Object cle = example;

		try {
			
			
			logger.trace("Object before handling: {}", BeanUtils.describe(example).toString());
			cle = BeanUtils.cloneBean(example);
			Field[] fields = cle.getClass().getDeclaredFields();
			Method[] methods = cle.getClass().getDeclaredMethods();
			for (Field field : fields) {
				boolean isFuzzyField = false;//field.isAnnotationPresent(FuzzyQuery.class);
				if (isFuzzyField) {
					trimFieldValue(example, field);
					trimAndFuzzyFieldValue(cle, field);
				}else{
					trimFieldValue(example, field);
					trimFieldValue(cle, field);
				}
				
				/*if ((IEntity.class).isAssignableFrom(field.getType())){
					logger.trace("Found PO attribute {}, start traversing PO field.", field.getName());
					Object value = null;
					field.setAccessible(true);
					value = field.get(example);
					if(value!=null){
						Object newVal = traverseCriteriaObject(value, checkList);
						field.set(cle, newVal);
					}
				} */
			}
			//handle annotation in getter methods...
			/*for (Method method : methods) {
				method.setAccessible(true);
				boolean isFuzzyField = method.isAnnotationPresent(FuzzyQuery.class);
				if (isFuzzyField) {
					if (method.getReturnType().isAssignableFrom(String.class)) {
						Object value = null;
						method.setAccessible(true);
						value = method.invoke(example);

						String property = StringUtils.uncapitalize(method.getName().replaceFirst("get", StringUtils.EMPTY));
						if(!StringUtils.isBlank((String)value)){
							Object newVal = "%" + StringUtils.trimToEmpty((String)value) + "%";
							BeanUtils.setProperty(cle, property, newVal);
						}
					}
				}
			}*/
			logger.trace("Object after handling: {}", BeanUtils.describe(cle).toString());
		} catch (Exception e) {
			logger.error("Error handling fuzzy query.", e);
		}
		
		return cle;
		
	}
	
	private void trimFieldValue(Object example, Field field) throws Exception{
		if (field.getType().isAssignableFrom(String.class)) {
			Object value = null;
			field.setAccessible(true);
			value = field.get(example);
			if(!StringUtils.isBlank((String)value)){
				Object newVal = StringUtils.trimToEmpty((String)value);
				field.set(example, newVal);
			}
		}
	}


	private void trimAndFuzzyFieldValue(Object example, Field field) throws Exception{
		if (field.getType().isAssignableFrom(String.class)) {
			Object value = null;
			field.setAccessible(true);
			value = field.get(example);
			if(!StringUtils.isBlank((String)value)){
				Object newVal = "%" + StringUtils.trimToEmpty((String)value)+ "%";
				field.set(example, newVal);
			}
		}
	}
	
	public static String columnResultToString (Object object) {
		return object !=null ? StringUtils.trimToEmpty(object.toString()) : StringUtils.EMPTY;
	}
	
	public static Integer columnResultToInteger (Object object) {
		return object !=null ? (Integer)object : null;
	}
	
	public static Date columnResultToDate (Object object) {
		return object !=null ? (Date)object : null;
	}

	@Override
	public int countByExample(E example) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public List<E> findByCriteria(com.inibb.app.dao.criteria.Criteria criteria) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public List<E> findByExample(E example) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public List<E> findByNamedQuery(String namedQuery) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public List<E> get(IInheritable<E> parent) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public boolean isActivable() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean isHiddenable() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean isInheritable() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public E load(PK id) {
		// TODO Auto-generated method stub
		return null;
	}

}
